﻿using System;
using System.Diagnostics;
using System.Drawing;
using System.Windows.Forms;
using Ignitron.EEngine.Adapters.DX.System;
using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.Windows;
using Buffer = SharpDX.Direct3D11.Buffer;
using Device = SharpDX.Direct3D11.Device;
using MapFlags = SharpDX.Direct3D11.MapFlags;

namespace Ignitron.EEngine.Adapters.DX.Graphics
{
    /// <summary>
    /// Class containing basic methods for working with DX11
    /// </summary>
    internal class DX11 
    {
        private bool mVerticalSyncEnabled;

        private SwapChain mSwapChain;
        private Device mDevice;
        private DeviceContext mDeviceContext;

        private RenderTargetView mRenderTargetView;
        private RasterizerState mRasterState;

        private Texture2D mDepthStencilBuffer;
        private DepthStencilState mDepthStencilState;
        private DepthStencilView mDepthStencilView;

        /// <summary>
        /// Returns the DirectX device
        /// </summary>
        public Device Device
        {
            get { return mDevice; }
        }


        #region Init / Shutdown
        
        public bool Initialize(SystemConfiguration configuration, IntPtr windowHandle)
        {
            try
            {
                // Store the vsync setting.
                mVerticalSyncEnabled = SystemConfiguration.VerticalSyncEnabled;

                // Create a DirectX graphics interface factory.
                var factory = new Factory();
                // Use the factory to create an adapter for the primary graphics interface (video card).
                var adapter = factory.GetAdapter(0);
                // Get the primary adapter output (monitor).
                var monitor = adapter.GetOutput(0);
                // Get modes that fit the DXGI_FORMAT_R8G8B8A8_UNORM display format for the adapter output (monitor).
                var modes = monitor.GetDisplayModeList(Format.R8G8B8A8_UNorm, DisplayModeEnumerationFlags.Interlaced);
                // Now go through all the display modes and find the one that matches the screen width and height.
                // When a match is found store the the refresh rate for that monitor, if vertical sync is enabled. 
                // Otherwise we use maximum refresh rate.
                var rational = new Rational(0, 1);
                if (mVerticalSyncEnabled)
                {
                    foreach (var mode in modes)
                    {
                        if (mode.Width == configuration.Width && mode.Height == configuration.Height)
                        {
                            rational = new Rational(mode.RefreshRate.Numerator, mode.RefreshRate.Denominator);
                            break;
                        }
                    }
                }

                Utilities.Dispose(ref monitor);
                Utilities.Dispose(ref adapter);
                Utilities.Dispose(ref factory);

                // Initialize the swap chain description.
                var swapChainDesc = new SwapChainDescription()
                {
                    // Set to a single back buffer.
                    BufferCount = 1,
                    // Set the width and height of the back buffer.
                    ModeDescription = new ModeDescription(configuration.Width, configuration.Height, rational, Format.R8G8B8A8_UNorm),
                    // Set the usage of the back buffer.
                    Usage = Usage.RenderTargetOutput,
                    // Set the handle for the window to render to.
                    OutputHandle = windowHandle,
                    // Turn multisampling off.                     
                    SampleDescription = new SampleDescription(1, 0),
                    // Set to full screen or windowed mode.
                    IsWindowed = !SystemConfiguration.FullScreen,
                    // Don't set the advanced flags.
                    Flags = SwapChainFlags.None,
                    // Discard the back buffer content after presenting.
                    SwapEffect = SwapEffect.Sequential                     
                };

                // Create the swap chain, Direct3D device, and Direct3D device context.
                Device device;
                SwapChain swapChain;
                Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.None, swapChainDesc, out device, out swapChain);

                mDevice = device;
                mSwapChain = swapChain;
                mDeviceContext = device.ImmediateContext;

                // Get the pointer to the back buffer.
                var backBuffer = Texture2D.FromSwapChain<Texture2D>(mSwapChain, 0);

                // Create the render target view with the back buffer pointer.
                mRenderTargetView = new RenderTargetView(device, backBuffer);

                // Release pointer to the back buffer as we no longer need it.
                backBuffer.Dispose();

                // Initialize and set up the description of the depth buffer.
                var depthBufferDesc = new Texture2DDescription()
                {
                    Width = configuration.Width,
                    Height = configuration.Height,
                    MipLevels = 1,
                    ArraySize = 1,
                    Format = Format.D24_UNorm_S8_UInt,
                    SampleDescription = new SampleDescription(1, 0),
                    Usage = ResourceUsage.Default,
                    BindFlags = BindFlags.DepthStencil,
                    CpuAccessFlags = CpuAccessFlags.None,
                    OptionFlags = ResourceOptionFlags.None
                };

                // Create the texture for the depth buffer using the filled out description.
                mDepthStencilBuffer = new Texture2D(device, depthBufferDesc);

                // Initialize and set up the description of the stencil state.
                var depthStencilDesc = new DepthStencilStateDescription()
                {
                    IsDepthEnabled = true,
                    DepthWriteMask = DepthWriteMask.All,
                    DepthComparison = Comparison.LessEqual,
                    IsStencilEnabled = true,
                    StencilReadMask = 0xFF,
                    StencilWriteMask = 0xFF,
                    // Stencil operation if pixel front-facing.
                    FrontFace = new DepthStencilOperationDescription()
                    {
                        FailOperation = StencilOperation.Keep,
                        DepthFailOperation = StencilOperation.Increment,
                        PassOperation = StencilOperation.Keep,
                        Comparison = Comparison.Always
                    },
                    // Stencil operation if pixel is back-facing.
                    BackFace = new DepthStencilOperationDescription()
                    {
                        FailOperation = StencilOperation.Keep,
                        DepthFailOperation = StencilOperation.Increment,
                        PassOperation = StencilOperation.Keep,
                        Comparison = Comparison.Always
                    }
                };

                // Create the depth stencil state.
                mDepthStencilState = new DepthStencilState(mDevice, depthStencilDesc);

                // Set the depth stencil state.
                mDeviceContext.OutputMerger.SetDepthStencilState(mDepthStencilState, 1);

                // Initialize and set up the depth stencil view.
                var depthStencilViewDesc = new DepthStencilViewDescription()
                {
                    Format = Format.D24_UNorm_S8_UInt,
                    Dimension = DepthStencilViewDimension.Texture2D,
                    Texture2D = new DepthStencilViewDescription.Texture2DResource()
                    {
                        MipSlice = 0
                    }
                };

                // Create the depth stencil view.
                mDepthStencilView = new DepthStencilView(mDevice, mDepthStencilBuffer, depthStencilViewDesc);

                // Bind the render target view and depth stencil buffer to the output render pipeline.
                mDeviceContext.OutputMerger.SetTargets(mDepthStencilView, mRenderTargetView);

                // Setup the raster description which will determine how and what polygon will be drawn.
                var rasterDesc = new RasterizerStateDescription()
                {
                    IsAntialiasedLineEnabled = true,
                    CullMode = CullMode.Back,
                    DepthBias = 0,
                    DepthBiasClamp = .0f,
                    IsDepthClipEnabled = true,
                    FillMode = FillMode.Solid,
                    IsFrontCounterClockwise = false,
                    IsMultisampleEnabled = true,                    
                    IsScissorEnabled = false,
                    SlopeScaledDepthBias = .0f
                };

                // Create the rasterizer state from the description we just filled out.
                mRasterState = new RasterizerState(mDevice, rasterDesc);

                // Now set the rasterizer state.
                mDeviceContext.Rasterizer.State = mRasterState;

                // Setup and create the viewport for rendering.
                mDeviceContext.Rasterizer.SetViewport(0, 0, configuration.Width, configuration.Height, 0, 1);

                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Enables the blendstate
        /// </summary>
        public void EnableBlendState()
        {
            RenderTargetBlendDescription blendDesc = new RenderTargetBlendDescription()
            {
                IsBlendEnabled = true,
                SourceBlend = BlendOption.SourceAlpha,
                DestinationBlend = BlendOption.InverseSourceAlpha,
                BlendOperation = BlendOperation.Add,
                SourceAlphaBlend = BlendOption.One,
                DestinationAlphaBlend = BlendOption.Zero,
                AlphaBlendOperation = BlendOperation.Add,
                RenderTargetWriteMask = ColorWriteMaskFlags.All
            };

            BlendStateDescription bstateDesc = new BlendStateDescription()
            {
                IndependentBlendEnable = false,
                AlphaToCoverageEnable = false,
            };
            bstateDesc.RenderTarget[0] = blendDesc;

            var blendState = new BlendState(mDevice, bstateDesc);

            mDeviceContext.OutputMerger.SetBlendState(blendState);
        }

        /// <summary>
        /// Disabled the blend state
        /// </summary>
        public void DisableBlendState()
        {
            RenderTargetBlendDescription blendDesc = new RenderTargetBlendDescription()
            {
                IsBlendEnabled = false,
                SourceBlend = BlendOption.SourceAlpha,
                DestinationBlend = BlendOption.InverseSourceAlpha,
                BlendOperation = BlendOperation.Add,
                SourceAlphaBlend = BlendOption.One,
                DestinationAlphaBlend = BlendOption.Zero,
                AlphaBlendOperation = BlendOperation.Add,
                RenderTargetWriteMask = ColorWriteMaskFlags.All
            };

            BlendStateDescription bstateDesc = new BlendStateDescription()
            {
                IndependentBlendEnable = false,
                AlphaToCoverageEnable = false,
            };
            bstateDesc.RenderTarget[0] = blendDesc;

            var blendState = new BlendState(mDevice, bstateDesc);

            mDeviceContext.OutputMerger.SetBlendState(blendState);
        }

        /// <summary>
        /// Shutdowns the DirectX system
        /// </summary>
        public void Shutdown()
        {
            Utilities.Dispose(ref mRasterState);
            Utilities.Dispose(ref mDepthStencilView);
            Utilities.Dispose(ref mDepthStencilState);
            Utilities.Dispose(ref mDepthStencilBuffer);
            Utilities.Dispose(ref mRenderTargetView);
            Utilities.Dispose(ref mDevice);
            Utilities.Dispose(ref mSwapChain);
        }



        #endregion

        #region API Methods

        /// <summary>
        /// Resizing the backbuffers
        /// </summary>
        public void Resize(int width, int height)
        {
            // get buffers description
            var scDesc = mSwapChain.Description;

            // Dispose all previous allocated resources
            Utilities.Dispose(ref mRenderTargetView);
            Utilities.Dispose(ref mDepthStencilView);

            // resize the buffers
            mSwapChain.ResizeBuffers(scDesc.BufferCount, width, height, scDesc.ModeDescription.Format, SwapChainFlags.AllowModeSwitch);

            // Get the backbuffer from the swapchain
            var _backBufferTexture = mSwapChain.GetBackBuffer<Texture2D>(0);
            // Backbuffer
            mRenderTargetView = new RenderTargetView(mDevice, _backBufferTexture);
            _backBufferTexture.Dispose();

            // Depth buffer
            var _zbufferTexture = new Texture2D(mDevice, new Texture2DDescription()
            {
                Format = Format.D24_UNorm_S8_UInt,
                ArraySize = 1,
                MipLevels = 1,
                Width = width,
                Height = height,
                SampleDescription = new SampleDescription(1, 0),
                Usage = ResourceUsage.Default,
                BindFlags = BindFlags.DepthStencil,
                CpuAccessFlags = CpuAccessFlags.None,
                OptionFlags = ResourceOptionFlags.None
            });


            // Create the depth buffer view
            mDepthStencilView = new DepthStencilView(mDevice, _zbufferTexture);
            _zbufferTexture.Dispose();

            // Setup targets and viewport for rendering
            mDeviceContext.Rasterizer.SetViewport(0, 0, width, height);
            mDeviceContext.OutputMerger.SetTargets(mDepthStencilView, mRenderTargetView);
        }


        /// <summary>
        /// Wrapper method for beginning rendering
        /// </summary>
        public void BeginScene(float red, float green, float blue, float alpha)
        {
            BeginScene(new Color4(red, green, blue, alpha));
        }

        /// <summary>
        /// Begins rendering
        /// </summary>
        public void BeginScene(Color4 color)
        {
            // Clear the depth buffer.
            mDeviceContext.ClearDepthStencilView(mDepthStencilView, DepthStencilClearFlags.Depth, 1f, 0);

            // Clear the back buffer.
            mDeviceContext.ClearRenderTargetView(mRenderTargetView, color);
        }

        /// <summary>
        /// Terminates the rendering
        /// </summary>
        public void EndScene()
        {
            // Present the back buffer to the screen since rendering is complete.
            if (mVerticalSyncEnabled)
            {
                // Lock to screen refresh rate.
                mSwapChain.Present(1, PresentFlags.None);
            }
            else
            {
                // Present as fast as possible.
                mSwapChain.Present(0, PresentFlags.None);
            }
        }

        public void SetDefaultTargets(int width, int height)
        {
            // Bind the render target view and depth stencil buffer to the output render pipeline.
            mDeviceContext.OutputMerger.SetTargets(mDepthStencilView, mRenderTargetView);

            // Setup and create the viewport for rendering.
            mDeviceContext.Rasterizer.SetViewport(0, 0, width, height);
        }

        #endregion
    }
}
